/*
    Copyright 2010 Northbranchlogic, Inc.

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
 */

package com.norbl.runjrun;

import com.norbl.util.*;
import com.norbl.util.ec2.*;
import com.xerox.amazonws.ec2.*;
import java.util.*;

/** Monitors ec2 instances.  This class in analogous to {@link TaskStateTable}
 *  and {@link NodeStateTable}.  However, it does not utilize sdb; it
 *  gets information by querying ec2 using Typica.<p>
 *
 *  <tt>InstanceMonitor</tt> maintains a list of instance states. The
 *  list is periodically updated.  All the action is in the <tt>run()</tt>
 *  method.  It is typically run as a separate thread.<p>
 *
 *  <tt>InstanceMonitor</tt> only tracks instances in an list
 *  specified in the constructor. It does not watch all of users instances.<p>
 *
 *  Optionally, <tt>InstanceMonitor</tt> can use an {@link InstanceSpec}
 *  request more instances if something goes wrong.
 *
 *
 * @author Barnet Wagman
 */
public class InstanceMonitor implements Runnable {

    /* Instance state:<br>
     *    pending, running, shutting-down, terminated
     * (strings in typica).<br>     
     */

        /** States are defined per the AWS EC2 api for
         *  "RunningInstanceItemType/InstanceType".
         */
    private static enum InstanceState {
        pending, running, shutting_down, terminated;

        public static InstanceState stateOf(String stateName) {
            if ( "shutting-down".equals(stateName) )
                return(shutting_down);
            else return( valueOf(stateName) );
        }

        boolean isRunning() {
            return( this.equals(InstanceState.running) );
        }
    }

    private Ec2Ops eops;
    private long updateIntervalMillis;

    private InstanceList iList;

    private boolean keepRunning;

        /** Specifes the total number of instances desired
         *  and their specification. This object is used
         *  to request more instances if something goes wrong.
         *  If it's value is null, instances are not requested
         *  whatever the conditions.
         */
    private InstanceSpec instanceSpec;

        /**
         *
         * @param eops
         * @param updateIntervalMillis
         * @param ris
         */
    public InstanceMonitor(Ec2Ops eops, long updateIntervalMillis,
                           List<ReservationDescription.Instance> ris) {
        this.eops = eops;
        this.updateIntervalMillis = updateIntervalMillis;

        iList = new InstanceList();

        synchronized(iList) {
            for ( ReservationDescription.Instance r : ris ) {
               iList.add(r);
            }         
        }
    }

        /** The {@link InstanceSpec} specifes the total number of
         *  instances desired
         *  and their specification. This object is used
         *  to request more instances if something goes wrong.
         *  If it's value is null, instances are not requested.
         */
    public void setInstanceSpec(InstanceSpec spec) {
        this.instanceSpec = spec;
    }

    public void stopRequestingInstancesAsNeeded() {
        instanceSpec = null;
    }

    public int getNRunning() { return(iList.countRunning()); }
    

    public List<String> getInstanceIDs() {
        List<String> iids = new ArrayList<String>();
        synchronized(iList) {            
            for ( ReservationDescription.Instance r : iList.list ) {
                if ( r != null ) {
                    String iid = r.getInstanceId();
                    if ( iid != null ) iids.add(iid);
                }
            }            
        }
        return(iids);
    }

    public List<String> getInstanceIDsRunningOrPending() {
        List<String> iids = new ArrayList<String>();
        synchronized(iList) {            
            for ( ReservationDescription.Instance r : iList.list ) {
                if ( r != null ) {
                    if ( isRunning(r) || isPending(r) ) {
                        String iid = r.getInstanceId();
                        if ( iid != null ) iids.add(iid);
                    }
                }
            }           
        }
        return(iids);
    }

    public void run() {
        keepRunning = true;
        while (keepRunning) {
            try {
                update();
                if ( instanceSpec != null ) {
                    int nRequested = requestInstancesIfNeeded();
                    if ( nRequested > 0 ) update();
                }               
                Time.nap(updateIntervalMillis);
            }
            catch(Throwable xxx) {
                System.out.println(StringUtil.throwableToString(xxx));
            }
        }

    }

    public void shutdown() {
        keepRunning = false;
        instanceSpec = null;
    }

    public boolean isShutdown() {
        return( !keepRunning && (instanceSpec == null) );
    }

    public void update() throws Ec2OpException {
        try {
            synchronized(iList) {

                List<ReservationDescription.Instance> sii = eops.getInstances();

                for ( ReservationDescription.Instance r : sii ) {
                    iList.update(r);
                    // ^ instances in the list are update but other are NOT
                    //   added.  The prevents unrelated non-rjr instances
                    //   that belong to the user from being listed.
                }
                
                List<ReservationDescription.Instance> ins =
                                            eops.getInstances();
                for ( ReservationDescription.Instance ri : ins ) {
                    iList.update(ri);
                }
            }

//             /* D */ System.out.println("InstanceMonitor.update() " +
//                       "n spot instances=" + siList.list.size() +
//                       ", live=" + siList.countLive() +
//                       ", running=" + siList.countRunning());
                           
        }
        catch(EC2Exception ex) { throw new Ec2OpException(ex); }
    }

        // -----------------------------------------------

    class InstanceList {

        List<ReservationDescription.Instance> list;
        HashMap<String,ReservationDescription.Instance> instanceHt;
       
        InstanceList() {
            list = new ArrayList<ReservationDescription.Instance>();           
            instanceHt = new HashMap<String,ReservationDescription.Instance>();
        }

        void add(ReservationDescription.Instance s) {
            if ( s == null ) return;
            if ( !instanceHt.containsKey(s.getInstanceId()) ) {
                list.add(s);
            }
            instanceHt.put(s.getInstanceId(),s);            
        }

            /** Update the instances state information, if <tt>r</tt>
             *  is not already in the list.  This method will <i>not</i>
             *  add an instance.
             * @param r
             */
        void update(ReservationDescription.Instance r) {
            if ( r == null ) return;
            if ( instanceHt.containsKey(r.getInstanceId()) )
                instanceHt.put(r.getInstanceId(),r);
        }        

        int countRunning() {
            int n = 0;
            for ( ReservationDescription.Instance r : list ) {
                if ( isRunning(r) ) ++n;
            }
            return(n);
        }

        int countLive() {
            int n = 0;
            for ( ReservationDescription.Instance r : list ) {
                if ( isRunning(r) || isPending(r) ) {
                    ++n;
                }
            }
            return(n);
        }
    }

    boolean isRunning(ReservationDescription.Instance ri) {
        if ( ri == null ) return(false);
        else return(InstanceState.running.toString().equals(ri.getState()));
    }

    boolean isPending(ReservationDescription.Instance ri) {
        if ( ri == null ) return(false);
        else return(InstanceState.pending.toString().equals(ri.getState()));
    }

        // ---------------------------------------------

    private int requestInstancesIfNeeded() throws EC2Exception {

        if ( instanceSpec == null ) return(0);
        synchronized(iList) {
            int nNeeded = instanceSpec.getNInstancesDesired() - iList.countLive();            
            if ( nNeeded > 0 ) {
                List<ReservationDescription.Instance> added =
                    instanceSpec.requestInstances(eops,nNeeded);
                for ( ReservationDescription.Instance r : added ) {
                    iList.add(r);
                }
                /* D */ System.out.println("InstanceMonitor requested " +
                            nNeeded + " extra instances.");
                return(added.size());
            }
            else return(0);
        }
    }
}
